using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace Tyng.Data.Utilities
{
    public sealed class SeedDataParser : ISeedDataParser
    {
        public SeedDataParser() { }

        #region ISeedDataParser Members

        void ISeedDataParser.Parse(ScriptList sl, List<Script> scripts, Database db, string file, TextReader configText, TextReader seedText, Stack<string> stack)
        {
            //check for config
            SeedDataType seedData = null;

            DateTime lastWriteTime = DateTime.Now;

            if (!string.IsNullOrEmpty(file))
                lastWriteTime = File.GetLastWriteTime(file);

            using (XmlReader xml = XmlReader.Create(configText))
            {
                XmlSerializer serializer = new XmlSerializer(typeof(SeedDataType));
                seedData = (SeedDataType)serializer.Deserialize(xml);
            }

            if (seedData.BeginSql != null && seedData.BeginSql.Length > 0)
            {
                throw new NotImplementedException("TODO: server type sql selection");
                //if (!string.IsNullOrEmpty(seedData.BeginSql))
                //    scripts.Add(new Script(db, file, lastWriteTime, seedData.BeginSql));
            }

            StringBuilder script = new StringBuilder();
            script.AppendLine("set nocount on");
        

            if (seedData != null && seedData.Table.Length > 0)
            {
                if (sl != null)
                {
                    AddSeedDataDependencies(db, seedData, stack, sl);
                }

                AddVariableDeclarationsAndGlobals(seedData, script);

                using (IDataReader dr = new CharacterDelimitedDataReader(seedText, true, ",", seedData.HeadersInCsv, seedData.CsvDataReaderFields))
                {
                    while (dr.Read())
                    {
                        AddPerRowVariables(seedData, dr, script);

                        foreach (TableType table in seedData.Table)
                        {
                            ProcessRow(seedData, table, dr, script);
                        }
                    }
                }
            }
            else
                throw new NotSupportedException(string.Format("A {0} file is required for seed data. (name: {1})", Constants.ConfigFileExtension, file));

            script.AppendLine("set nocount off");

            scripts.Add(new Script(db, file, lastWriteTime, script.ToString()));

            if (seedData.EndSql != null && seedData.EndSql.Length > 0)
            {
                throw new NotImplementedException("TODO: server type sql selection");
                //if (!string.IsNullOrEmpty(seedData.EndSql))
                //    scripts.Add(new Script(db, file, lastWriteTime, seedData.EndSql));
            }
        }

        private void ProcessRow(SeedDataType seedData, TableType table, IDataReader dr, StringBuilder script)
        {
            if (!string.IsNullOrEmpty(table.InsertIfVariable))
            {
                script.AppendFormat("if isnull({0}{1},0)=1\n", Constants.VariablePrefix, table.InsertIfVariable);
            }

            if (!table.NoExistsCheck && !string.IsNullOrEmpty(table.ExistsWhere))
            {
                script.AppendFormat("if not exists(select 1 from[{0}]where {1})\n", table.Name, ReplaceColumnVariables(table.ExistsWhere, dr, seedData.CsvColumn, table));
            }

            //insert data
            script.AppendFormat("insert[{0}]({1})values({2})\n", table.Name, table.InsertFields, ReplaceColumnVariables(table.InsertValues, dr, seedData.CsvColumn, table));
        }

        private void AddPerRowVariables(SeedDataType seedData, IDataReader dr, StringBuilder script)
        {
            if (seedData.Variable != null && seedData.Variable.Length > 0)
            {
                foreach (VariableType var in seedData.Variable)
                {
                    if (var.Scope == VariableScope.PerRow)
                    {
                        script.AppendFormat("set {0}{1}=null\n", Constants.VariablePrefix, var.Name);

                        throw new NotImplementedException("TODO: server type sql selection");
                        //string select = var.Sql;

                        //select = ReplaceColumnVariables(select, dr, seedData.CsvColumn, null);

                        //script.AppendLine(select);
                    }
                }
            }
        }

        private string ReplaceColumnVariables(string s, IDataReader dr, CsvColumnType[] columns, TableType table)
        {
            foreach (CsvColumnType col in columns)
            {
                string replaceValue;

                if (col.DataIsLiteral)
                    replaceValue = (string)dr[col.Name];
                else
                    replaceValue = TSqlHelper.GetStringRepresentation(dr[col.Name]);
          
                s = s.Replace(Constants.ColumnReplacePrefix + col.Name, replaceValue);
            }

            if (table != null)
            {
                foreach (TableColumnType col in table.Column)
                {
                    string replaceValue;

                    if (!string.IsNullOrEmpty(col.Literal))
                        replaceValue = col.Literal;
                    else if (! string.IsNullOrEmpty(col.UseVariable))
                        replaceValue = Constants.VariablePrefix + col.UseVariable;
                    else
                        replaceValue = TSqlHelper.GetStringRepresentation(dr[col.DataReaderName]);

                    s = s.Replace(Constants.ColumnReplacePrefix + col.Name, replaceValue);
                }
            }

            return s;
        }

        private void AddVariableDeclarationsAndGlobals(SeedDataType seedData, StringBuilder script)
        {
            if (seedData.Variable != null && seedData.Variable.Length > 0)
            {
                script.Append("declare ");

                bool first = true;

                foreach (VariableType var in seedData.Variable)
                {
                    if (!first)
                    {
                        script.Append(",");
                    }
                    else
                        first = false;

                    script.AppendFormat("{0}{1} {2}", Constants.VariablePrefix, var.Name, var.Type);
                }

                script.AppendLine();

                foreach (VariableType var in seedData.Variable)
                {
                   if (var.Scope == VariableScope.Global)
                   {
                       throw new NotImplementedException("TODO: server type sql selection");
                       //script.AppendLine(var.Sql);
                   }
                }
            }
        }

        private void AddSeedDataDependencies(Database db, SeedDataType seedData, Stack<string> stack, ScriptList sl)
        {
            if (seedData.Dependency != null && seedData.Dependency.Length > 0)
            {
                //Dependencies
                foreach (DependencyType depend in seedData.Dependency)
                {
                    Database dependDatabase = sl.Databases[depend.Database];

                    string fullFile = Path.Combine(dependDatabase.Path, depend.File);

                    if (!File.Exists(fullFile))
                        throw new Exception(string.Format("Dependency {0} does not exist", depend.File));

                    sl.AddFile(dependDatabase, stack, fullFile);
                }
            }
        }

        #endregion
    }

    public partial class SeedDataType
    {
        [NonSerialized]
        private bool _init;
        [NonSerialized]
        private List<Field> _fields;

        [XmlIgnore]
        public Field[] CsvDataReaderFields
        {
            get
            {
                Init();

                return _fields.ToArray();
            }
        }

        private void Init()
        {
            if (!_init)
            {
                _fields = new List<Field>();

                for (int i = 0; i < csvColumnField.Length; i++)
                {
                    Type t = csvColumnField[i].GetColumnType();

                    if (t == typeof(byte[]))
                        _fields.Add(new Field(csvColumnField[i].Name, t, HexFieldConverter.Default, true));
                    else
                        _fields.Add(new Field(csvColumnField[i].Name, t, true));
                }

                _init = true;
            }
        }
    }

    public partial class DependencyType
    {
        public override bool Equals(object obj)
        {
            if (obj is DependencyType)
            {
                DependencyType dt = (DependencyType)obj;

                if (dt.databaseField == databaseField && dt.fileField == fileField)
                    return true;
            }

            return false;
        }

        public override int GetHashCode()
        {
            return fileField.GetHashCode() ^ databaseField.GetHashCode();
        }
    }

    public partial class CsvColumnType
    {
        public Type GetColumnType()
        {
            if (DataIsLiteral)
                return typeof(string);
            else
            {
                Type t = Type.GetType(TypeAssemblyQualifiedName);

                if (t == null)
                    throw new TypeLoadException(string.Format("Unable to find type '{0}'", TypeAssemblyQualifiedName));

                return t;
            }
        }
    }

    public partial class TableColumnType
    {
        public string DataReaderName
        {
            get
            {
                if (CsvName != null && CsvName.Length > 0)
                    return CsvName;

                return Name;
            }
        }
    }

    public partial class TableType
    {
        [NonSerialized]
        private bool _init;
        [NonSerialized]
        private string _existsWhere;
        [NonSerialized]
        private string _insertFields;
        [NonSerialized]
        private string _insertValues;

        [XmlIgnore]
        public string ExistsWhere { get { Init(); return _existsWhere; } }
        [XmlIgnore]
        public string InsertFields { get { Init(); return _insertFields; } }
        [XmlIgnore]
        public string InsertValues { get { Init(); return _insertValues; } }

        private void Init()
        {
            if (!_init)
            {
                _insertFields = string.Empty;
                _existsWhere = string.Empty;
                _insertValues = string.Empty;

                for (int i = 0; i < columnField.Length; i++)
                {
                    if (_insertFields.Length > 0)
                        _insertFields += ",";

                    _insertFields += "[" + columnField[i].Name + "]";

                    if (_insertValues.Length > 0)
                        _insertValues += ",";

                    if (!string.IsNullOrEmpty(columnField[i].Literal))
                        _insertValues += columnField[i].Literal;
                    else
                        _insertValues += Constants.ColumnReplacePrefix + columnField[i].DataReaderName;

                    //find unique columns
                    if (columnField[i].IncludeInUnique)
                    {
                        if (_existsWhere.Length > 0)
                            _existsWhere += " AND ";

                        _existsWhere += string.Format("[{0}]={1}{0}", columnField[i].Name, Constants.ColumnReplacePrefix);
                    }
                }

                _init = true;
            }
        }
    }
}
